package com.fulihui.information.biz.content.impl.handler;

import com.fulihui.information.common.config.AppConst;
import com.fulihui.information.core.db.ContentListItemDB;
import com.fulihui.information.core.redis.RedisBasicCategoryContentSortDB;
import com.fulihui.information.core.redis.RedisHotsContentReadTimesSortDB;
import com.fulihui.information.core.redis.RedisHotsContentSharedTimesSortDB;
import com.fulihui.information.dto.ContentListItemDTO;
import com.fulihui.information.enums.EDataObtainType;
import com.fulihui.information.request.ContentPageQueryForAppRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

import java.util.*;

import static org.near.toolkit.common.StringUtil.EMPTY_STRING;
import static org.springframework.util.CollectionUtils.isEmpty;

/**
 * Reids分组随机获取数据分页展示 ... TODO doc
 * @author Willard.Hu on 2017/1/10 0010.
 */
@Component
public class RedisGroupRandomDataOrderlyAppListPageObtainHandler extends AbstractAppListPageObtainHandler {
    /**
     * 普通类目取新数据限制秒数，排序中使用时间分钟数作为评分，3天
     */
    private static final int NEW_DATA_LIMIT_MINUTES     = 259200;
    /**
     * 热门类目取新数据限制秒数，排序中使用时间分钟数作为评分，1天
     */
    private static final int HOTS_NEWDATA_LIMIT_MINUTES = 86400;
    /**
     * 保证新数据的页码
     */
    private static final int NEW_DATA_LIMIT_PAGE        = 10;
    /**
     * 随机比率，大致多少次发生一次重复
     */
    private static final int RANDOM_RATIO               = 10;

    @Autowired
    private RedisBasicCategoryContentSortDB   redisBasicCategoryContentSortDB;
    @Autowired
    private RedisHotsContentReadTimesSortDB   redisHotsContentReadTimesSortDB;
    @Autowired
    private RedisHotsContentSharedTimesSortDB redisHotsContentSharedTimesSortDB;
    @Autowired
    @Qualifier("redisContentListItemDB")
    private ContentListItemDB                 contentListItemDB;

    private Random random = new Random();

    /**
     * 普通数据分3天前和之后的历史数据
     * 热门数据分1天前和之后的历史数据
     * NEW_DATA_LIMIT_PAGE 页前使用新数据，不够则取旧数据，剩下的页数直接取旧数据
     */
    @Override
    List<ContentListItemDTO> queryPageList(ContentPageQueryForAppRequest request) {
        List<String> ids = new ArrayList<>(request.getRows());
        // 指定 NEW_DATA_LIMIT_PAGE 的页数需要取新，后续页数可以从历史数据中捞取
        boolean isnew = request.getPage() < NEW_DATA_LIMIT_PAGE + 1;
        int dataSize = request.getRows() * RANDOM_RATIO;
        int maxNewDataSize = dataSize * NEW_DATA_LIMIT_PAGE;
        Set<String> query; // XXX 计算过程中偶尔会出现重复数据，目前使用Set数据类型处理去重，等排查出真正原因再修改
        if (AppConst.CONTENT_HOTS_CATEGORY_CODE.equals(request.getCategoryCode())) {
            query = new HashSet<>(getHotsContentIds(isnew, dataSize, maxNewDataSize));
            // 取新时，数据量不够，直接获取旧组数据做补偿，第一页直接返回全量，无需考虑
            if (isnew && query.size() < dataSize) {
                // 第一页数据不足，补上足够的数据
                if (request.getPage() == 1 && query.size() < request.getRows()) {
                    query.addAll(getHotsContentIds(false, dataSize, maxNewDataSize));
                } else {
                    query = new HashSet<>(getHotsContentIds(false, dataSize, maxNewDataSize));
                }
            }
        } else {
            query = new HashSet<>(getCategoryContentIds(isnew,
                    request.getCategoryCode(), dataSize, maxNewDataSize));
            // 取新时，数据量不够，直接获取旧组数据做补偿，第一页直接返回全量，无需考虑
            if (isnew && query.size() < dataSize) {
                // 第一页如果是一页数据都不满足，说明新数据不够，则对第一页追加就数据，否则什么都不处理
                if (request.getPage() == 1) {
                    if (query.size() < request.getRows()) {
                        // 补的数据
                        List<String> fill = getCategoryContentIds(false, request.getCategoryCode(),
                                dataSize, maxNewDataSize);
                        fill.removeAll(query); // 去重
                        query.addAll(fill);
                    }
                } else { // 非第一页直接获取就数据中的
                    query = new HashSet<>(getCategoryContentIds(false, request.getCategoryCode(),
                            dataSize, maxNewDataSize));
                }
            }
        }
        // 去重处理
        if (!isEmpty(request.getLastQueries())) {
            query.removeAll(request.getLastQueries());
        }
        List<ContentListItemDTO> items;
        // 第一页待随机的数据总量小于可随机遍历dataSize条，直接全量传给前段，由前端随机分页处理
        if (request.getPage() == 1 && query.size() < dataSize) {
            items = contentListItemDB.queryList(new ArrayList<>(query));
        } else {
            if (query.size() > 0) {
                // 随机抽取数据
                List<String> tmp = new ArrayList<>(query); // XXX 由于去重需要使用了Set数据结构，问题解决可以不使用tmp，直接使用query
                for (int i = 0; i < request.getRows() && tmp.size() > 0; i++) {
                    int idx = random.nextInt(tmp.size());
                    ids.add(tmp.remove(idx));
                }
                items = contentListItemDB.queryList(ids);

                // FIXME [huwei] 临时处理热点有段子的问题，排查出问题后将此段代码删除
                if (AppConst.CONTENT_HOTS_CATEGORY_CODE.equals(request.getCategoryCode())) {
                    for (int i = items.size() - 1; i >= 0; i--) {
                        if (AppConst.CONTENT_ESSAY_JOKE_CATEGORY_CODE.equals(items.get(i).getCategoryCode())) {
                            items.remove(i);
                        }
                    }
                }

            } else {
                // emptyList不能再被插入，考虑到后续top的插入，所以初始化空集合
                items = new ArrayList<>(0);
            }
        }
        items.forEach(it -> it.setOrdid(it.getId()));
        return items;
    }

    @Override
    int queryTotalCount(ContentPageQueryForAppRequest request) {
        int totalCount;
        if (AppConst.CONTENT_HOTS_CATEGORY_CODE.equals(request.getCategoryCode())) {
            int n = (int) redisHotsContentReadTimesSortDB.size(EMPTY_STRING);
            int m = (int) redisHotsContentSharedTimesSortDB.size(EMPTY_STRING);
            // 取阅读数、分享数中数据最多的
            totalCount = n > m ? n : m;
        } else {
            totalCount = (int) redisBasicCategoryContentSortDB.size(request.getCategoryCode());
        }
        return totalCount;
    }

    @Override
    public EDataObtainType obtainType() {
        return EDataObtainType.GROUP_RANDOM_DATE_ORDERLY;
    }

    private List<String> getHotsContentIds(boolean isnew, int dataSize, int maxNewDataSize) {
        double min = System.currentTimeMillis() / 1000 - HOTS_NEWDATA_LIMIT_MINUTES;
        // 阅读数、分享数都获取下
        // 新阅读数、分享数的数量
        int newDataSize = (int) redisHotsContentReadTimesSortDB.rangeByScoreCount(
                min, Double.MAX_VALUE, EMPTY_STRING);
        newDataSize = newDataSize > maxNewDataSize ? maxNewDataSize : newDataSize;
        int newDataSize2 = (int) redisHotsContentSharedTimesSortDB.rangeByScoreCount(
                min, Double.MAX_VALUE, EMPTY_STRING);
        newDataSize2 = newDataSize2 > maxNewDataSize ? maxNewDataSize : newDataSize2;
        List<String> res = new LinkedList<>();
        Set<String> data;
        if (isnew) {
            // 阅读数
            Range range = newDataRange(dataSize, newDataSize);
            data = redisHotsContentReadTimesSortDB.range(range.start, range.end, EMPTY_STRING);
            if (!isEmpty(data)) {
                res.addAll(data);
            }
            // 分享数
            range = newDataRange(dataSize, newDataSize2);
            data = redisHotsContentSharedTimesSortDB.range(range.start, range.end, EMPTY_STRING);
            if (!isEmpty(data)) {
                res.addAll(data);
            }
        } else {
            // 阅读数
            int totalCount = (int) redisHotsContentReadTimesSortDB.size(EMPTY_STRING);
            Optional<Range> range = oldDataRange(newDataSize, dataSize, totalCount);
            if (range.isPresent()) {
                data = redisHotsContentReadTimesSortDB.range(range.get().start, range.get().end, EMPTY_STRING);
                if (!isEmpty(data)) {
                    res.addAll(data);
                }
            }
            // 分享数
            int totalCount2 = (int) redisHotsContentSharedTimesSortDB.size(EMPTY_STRING);
            range = oldDataRange(newDataSize2, dataSize, totalCount2);
            if (range.isPresent()) {
                data = redisHotsContentReadTimesSortDB.range(range.get().start, range.get().end, EMPTY_STRING);
                if (!isEmpty(data)) {
                    res.addAll(data);
                }
            }
        }
        return res;
    }

    private List<String> getCategoryContentIds(boolean isnew, String categoryCode, int dataSize, int maxNewDataSize) {
        double min = System.currentTimeMillis() / 1000 - NEW_DATA_LIMIT_MINUTES;
        // 新数据的数量
        int newDataSize = (int) redisBasicCategoryContentSortDB.rangeByScoreCount(min, Double.MAX_VALUE, categoryCode);
        newDataSize = newDataSize > maxNewDataSize ? maxNewDataSize : newDataSize;
        Set<String> res = null;
        if (isnew) {
            Range range = newDataRange(dataSize, newDataSize);
            res = redisBasicCategoryContentSortDB.range(range.start, range.end, categoryCode);
        } else {
            int totalCount = (int) redisBasicCategoryContentSortDB.size(categoryCode);
            Optional<Range> range = oldDataRange(newDataSize, dataSize, totalCount);
            if (range.isPresent()) {
                res = redisBasicCategoryContentSortDB.range(range.get().start, range.get().end, categoryCode);
            }
        }
        return isEmpty(res) ? new ArrayList<>() : new ArrayList<>(res);
    }

    /**
     * 随机取 0 ~ (size - dataSize) 作为起始位，如果 size <= dataSize，起始位只能为0
     * @param dataSize 要获取的数据量
     * @param size 新数据总量
     * @return 起始位和结束位
     */
    private Range newDataRange(int dataSize, int size) {
        int start = size > dataSize ? random.nextInt(size - dataSize) : 0;
        // 新数据的数量不大于需要获取数量，值返回新数据总量
        int end = size > dataSize ? start + dataSize : size;
        log.info("newDataRange ===> dataSize:{}, size:{}, start:{}, end:{}", new Object[]{
                dataSize, size, start, end});
        return new Range(start, end);
    }

    /**
     * 随机取 startIdx ~ (size - dataSize) 作为起始位，如果 startIdx >= size 说明没有可取数据了，
     * 如果 (size - startIdx) <= dataSize，起始位只能为0，最后起始位再加上 startIdx，位移到旧数据区间。
     * @param startIdx 旧数据起始位
     * @param dataSize 要获取的数据量
     * @param size 所有数据总量
     * @return 起始位和结束位
     */
    private Optional<Range> oldDataRange(int startIdx, int dataSize, int size) {
        int oldSize = size - startIdx;
        if (oldSize <= 0) {
            log.warn("oldDataRange ===> startIdx:{}, size:{} 没有旧数据了", startIdx, size);
            return Optional.empty();
        }
        int start = oldSize > dataSize ? random.nextInt(oldSize - dataSize) : 0;
        start += startIdx;
        int end = start + dataSize;
        log.info("oldDataRange ===> startIdx:{}, dataSize:{}, size:{}, start:{}, end:{}", new Object[]{
                startIdx, dataSize, size, start, end});
        return Optional.of(new Range(start, end));
    }

    private class Range {
        int start;
        int end;

        Range(int start, int end) {
            this.start = start;
            this.end = end;
        }
    }
}
